#include "fmt.h"

#include "mem.h"
#include "assert.h"

#include <stddef.h>
#include <stdio.h>
#include <limits.h>
#include <string.h>
#include <ctype.h>
#include <float.h>

#define T Fmt_T

// See Fmt_putd
// '-': left-justified in given width, otherwise right-justified.
// '+': a sign to be '+' or '-'.
// ' ': a sign to be ' ' if it's positive.
// '0': number to be padded with leading zeros.
const char *Fmt_flags = "-+ 0";
const Except_T Fmt_Overflow = {"Formatting Overflow"};

#define normalize_width_precision(width, precision) { \
    if (width == INT_MIN) \
        width = 0; \
    if (width < 0) { \
        flags['-'] = 1; \
        width = -width; \
    } \
    if (precision >= 0) \
        flags['0'] = 0; \
}

#define pad(n, c) { \
    int v = n; \
    while (v-- > 0) \
        put((c), cl); \
}


struct buf {
    char *buf;
    char *bp;
    int size;
};

static int
outc(int c, void *cl)
{
    FILE *file = cl;
    return putc(c, file);
}

void Fmt_print(const char *fmt, ...)
{
    va_list_box box;
    va_start(box.ap, fmt);
    Fmt_vfmt(outc, stdout, fmt, &box);
    va_end(box.ap);
}

void Fmt_fprint(FILE *stream, const char *fmt, ...)
{
    va_list_box box;
    va_start(box.ap, fmt);
    Fmt_vfmt(outc, stream, fmt, &box);
    va_end(box.ap);
}

int Fmt_sfmt(char *buf, int size, const char *fmt, ...)
{
    va_list_box box;
    va_start(box.ap, fmt);
    int len = Fmt_vsfmt(buf, size, fmt, &box);
    va_end(box.ap);
    return len;
}

static int
insert(int c, void *cl)
{
    struct buf *p = cl;
    if (p->bp >= p->buf + p->size)
        RAISE(Fmt_Overflow);
    *p->bp++ = c;
    return c;
}

int Fmt_vsfmt(char *buf, int size, const char *fmt, va_list_box *box)
{
    assert(buf);
    assert(size > 0);
    assert(fmt);

    struct buf cl;
    cl.bp = cl.buf = buf;
    cl.size = size;

    Fmt_vfmt(insert, &cl, fmt, box);
    insert(0, &cl);
    return cl.bp - cl.buf - 1;
}

char* Fmt_string(const char *fmt, ...)
{
    va_list_box box;
    va_start(box.ap, fmt);
    char *str = Fmt_vstring(fmt, &box);
    va_end(box.ap);
    return str;
}

static int
append(int c, void *cl)
{
    struct buf *p = cl;
    if (p->bp >= p->buf + p->size) {
        RESIZE(p->buf, 2*p->size);
        p->bp = p->buf + p->size;
        p->size *= 2;
    }
    *p->bp++ = c;
    return c;
}

char* Fmt_vstring(const char *fmt, va_list_box *box)
{
    assert(fmt);

    struct buf cl;
    cl.size = 256;
    cl.bp = cl.buf = ALLOC(cl.size);
    Fmt_vfmt(append, &cl, fmt, box);
    append(0, &cl);
    return RESIZE(cl.buf, cl.bp - cl.buf); // to release alloced extra space
}

static void
cvt_s(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    char *str = va_arg(box->ap, char*);
    assert(str);
    Fmt_puts(str, strlen(str), put, cl, flags, width, precision);
}

static void
cvt_d(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    unsigned int m;
    int val = va_arg(box->ap, int);

    if (val == INT_MAX)
        m = INT_MAX + 1u;
    else if (val < 0)
        m = -val;
    else
        m = val;

    char buf[44]; // For octal, if max integer is 128 bit, 128  / 3
    char *p = buf + sizeof(buf);

    do {
        *--p = m % 10 + '0';
    } while ((m /= 10) > 0);
    if (val < 0)
        *--p = '-';

    int len = buf + sizeof(buf) - p;
    Fmt_putd(p, len, put, cl, flags, width, precision);
}

static void
cvt_u(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    unsigned int m = va_arg(box->ap, unsigned int);
    char buf[44];
    char *p = buf + sizeof(buf);

    do {
        *--p = m % 10 + '0';
    } while ((m /= 10) > 0);

    int len = buf + sizeof(buf) - p;
    Fmt_putd(p, len, put, cl, flags, width, precision);
}

static void
cvt_o(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    unsigned int m = va_arg(box->ap, unsigned int);
    char buf[44];
    char *p = buf + sizeof(buf);

    do {
        *--p = (m & 0x7) + '0';
    } while ((m >>= 3) > 0);

    int len = buf + sizeof(buf) - p;
    Fmt_putd(p, len, put, cl, flags, width, precision);
}

static void
cvt_x(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    unsigned int m = va_arg(box->ap, unsigned int);
    char buf[44];
    char *p = buf + sizeof(buf);

    do {
        *--p = "0123456789abcdef"[m & 0xF];
    } while ((m >>= 4) > 0);

    int len = buf + sizeof(buf) - p;
    Fmt_putd(p, len, put, cl, flags, width, precision);
}

static void
cvt_p(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    size_t m = (size_t)va_arg(box->ap, void*);
    char buf[44];
    char *p = buf + sizeof(buf);

    do {
        *--p = "0123456789abcdef"[m & 0xF];
    } while ((m >>= 4) > 0);

    precision = INT_MIN;
    int len = buf + sizeof(buf) - p;
    Fmt_putd(p, len, put, cl, flags, width, precision);
}

static void
cvt_c(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    normalize_width_precision(width, precision);
    if (!flags['-'])
        pad(width - 1, ' ');
    put((unsigned char)va_arg(box->ap, int), cl);
    if (flags['-'])
        pad(width - 1, ' ');
}

static void
cvt_f(int code, va_list_box *box,
      int put(int c, void *cl), void *cl,
      unsigned char flags[256], int width, int precision)
{
    (void)code;
    char buf[DBL_MAX_10_EXP+1+1+99+1]; // %.99f
    static char fmt[] = "%.dd?";

    if (precision < 0)
        precision = 6;
    if (code == 'g' && precision == 0)
        precision = 1;

    // Format a double argument into buf
    assert(precision <= 99);
    fmt[4] = code;
    fmt[3] = precision % 10 + '0';
    fmt[2] = (precision/10) % 10 + '0';
    snprintf(buf, sizeof(buf), fmt, va_arg(box->ap, double));

    Fmt_putd(buf, strlen(buf), put, cl, flags, width, precision);
}

// %d %o %u %x
// %c
// %e %f %g
// %p %s
static T cvt[256] = {
 /*   0-  7 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*   8- 15 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  16- 23 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  24- 31 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  32- 39 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  40- 47 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  48- 55 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  56- 63 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  64- 71 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  72- 79 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  80- 87 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  88- 95 */ 0,     0, 0,     0,     0,     0,     0,     0,
 /*  96-103 */ 0,     0, 0, cvt_c, cvt_d, cvt_f, cvt_f, cvt_f,
 /* 104-111 */ 0,     0, 0,     0,     0,     0,     0, cvt_o,
 /* 112-119 */ cvt_p, 0, 0, cvt_s,     0, cvt_u,     0,     0,
 /* 120-127 */ cvt_x, 0, 0,     0,     0,     0,     0,     0
};

void Fmt_fmt(int put(int c, void *cl), void *cl, const char *fmt, ...)
{
    va_list_box box;
    va_start(box.ap, fmt);
    Fmt_fmt(put, cl, fmt, &box);
    va_end(box.ap);
}

#define next_val_or_digit(fmt, box, val) { \
    int n = 0; \
    if (*fmt == '*') { \
        n = va_arg(box->ap, int); \
        assert(n != INT_MIN); \
        fmt++; \
    \
    } else { \
        for (n = 0; isdigit(*fmt); fmt++) { \
            int d = *fmt - '0'; \
            assert(10 * n + d <= INT_MAX); \
            n = n * 10 + d; \
        } \
    } \
    val = n; \
}

void Fmt_vfmt(int put(int c, void *cl), void *cl, const char *fmt, va_list_box *box)
{
    assert(put);
    assert(fmt);

	unsigned char flags[256] = {0};
	int width = INT_MIN, precision = INT_MIN;

    while (*fmt) {
        if (*fmt != '%' || *++fmt == '%') {
            put((unsigned char)(*fmt++), cl);

        } else {
	        // get optional flags
            unsigned char c;
            for (c = *fmt; c && strchr(Fmt_flags, c); c = *++fmt) {
                assert(flags[c]++ < 255);
            }
	        // get optional width
            if (*fmt == '*' || isdigit(*fmt)) {
                next_val_or_digit(fmt, box, width);
            }
            // get optional precision
            if (*fmt == '.' && (*++fmt == '*' || isdigit(*fmt))) {
                next_val_or_digit(fmt, box, precision);
            }

            c = *fmt++;
            assert(cvt[c]);
            cvt[c](c, box, put, cl, flags, width, precision);
        }
    }
}

T Fmt_register(int code, T newcvt)
{
    assert(code > 0 && code < (int)(sizeof(cvt)/sizeof(cvt[0])));
    T old = cvt[code];
    cvt[code] = newcvt;
    return old;
}

void Fmt_putd(const char *str, int len,
              int put(int c, void *cl), void *cl,
              unsigned char flags[256], int width, int precision)
{
    assert(str);
    assert(len > 0);
    assert(flags);

    normalize_width_precision(width, precision);

    // Compute sign
    int sign;
    if (len > 0 && (*str == '+' || *str == '-')) {
        sign = *str++;
        len--;
    } else if (flags['+']) {
        sign = '+';
    } else if (flags[' ']) {
        sign = ' ';
    } else {
        sign = 0;
    }

    int n;
    if (precision < 0)
        precision = 1;
    if (len < precision)
        n = precision;
    else if (precision == 0 && len == 1 && str[0] == '0')
        n = 0;
    else
        n = len;
    if (sign)
        n++;

    if (flags['-']) {
        if (sign)
            put(sign, cl);

    } else if (flags['0']) {
        if (sign)
            put(sign, cl);
        pad(width - n, '0');
    } else {
        pad(width - n, ' ');
        if (sign)
            put(sign, cl);
    }

    pad(precision - len, '0');
    for (int i = 0; i < len; i++)
        put((unsigned char)str[i], cl);
    if (flags['-'])
        pad(width - n, ' ');
}

void Fmt_puts(const char *str, int len,
              int put(int c, void *cl), void *cl,
              unsigned char flags[256], int width, int precision)
{
    assert(str);
    assert(len > 0);
    assert(flags);

    normalize_width_precision(width, precision);

    if (precision >= 0 && precision < len)
        len = precision;
    
    if (!flags['-'])
        pad(width - len, ' ');
    for (int i = 0; i < len; i++)
        put((unsigned char)str[i], cl);
    if (flags['-'])
        pad(width - len, ' ');
}

